/**
 * Vector helpers
 */
function vectorAdd(as, bs) {
  const cs = [];

  if (as.length !== bs.length) {
    return null;
  }

  for (let i = 0; i < as.length; i++) {
    cs.push(as[i] + bs[i]);
  }

  return cs;
}

function vectorMultiply(as, k) {
  const cs = [];

  for (let i = 0; i < as.length; i++) {
    cs.push(as[i] * k);
  }

  return cs;
}

function vectorSubtract(as, bs) {
  const cs = [];

  if (as.length !== bs.length) {
    return null;
  }

  for (let i = 0; i < as.length; i++) {
    cs.push(as[i] - bs[i]);
  }

  return cs;
}

/**
 * Create a d3 graph with Parallel Coordinates
 */
export function parcoords(config) {
  var __ = {
    data: [],
    highlighted: [],
    dimensions: {},
    dimensionTitleRotation: 0,
    brushed: false,
    brushedColor: null,
    alphaOnBrushed: 0.0,
    mode: 'default',
    rate: 20,
    width: 600,
    height: 300,
    margin: { top: 24, right: 0, bottom: 12, left: 0 },
    nullValueSeparator: 'undefined', // set to "top" or "bottom"
    nullValueSeparatorPadding: { top: 8, right: 0, bottom: 8, left: 0 },
    color: '#069',
    composite: 'source-over',
    alpha: 0.7,
    bundlingStrength: 0.5,
    bundleDimension: null,
    smoothness: 0.0,
    showControlPoints: false,
    hideAxis: [],
    flipAxes: [],
    animationTime: 1100, // How long it takes to flip the axis when you double click
    rotateLabels: false,
  };

  extend(__, config);

  if (config && config.dimensionTitles) {
    console.warn(
      'dimensionTitles passed in config is deprecated. Add title to dimension object.',
    );
    d3.entries(config.dimensionTitles).forEach(function(d) {
      if (__.dimensions[d.key]) {
        __.dimensions[d.key].title = __.dimensions[d.key].title
          ? __.dimensions[d.key].title
          : d.value;
      } else {
        __.dimensions[d.key] = {
          title: d.value,
        };
      }
    });
  }
  var pc = function(selection) {
    selection = pc.selection = d3.select(selection);

    __.width = selection[0][0].clientWidth;
    __.height = selection[0][0].clientHeight;

    // canvas data layers
    ['marks', 'foreground', 'brushed', 'highlight'].forEach(function(layer) {
      canvas[layer] = selection.append('canvas').attr('class', layer)[0][0];
      ctx[layer] = canvas[layer].getContext('2d');
    });

    // svg tick and brush layers
    pc.svg = selection
      .append('svg')
      .attr('width', __.width)
      .attr('height', __.height)
      .style('font', '14px sans-serif')
      .style('position', 'absolute')

      .append('svg:g')
      .attr(
        'transform',
        'translate(' + __.margin.left + ',' + __.margin.top + ')',
      );

    return pc;
  };
  var events = d3.dispatch.apply(
      this,
      [
        'render',
        'resize',
        'highlight',
        'brush',
        'brushend',
        'brushstart',
        'axesreorder',
      ].concat(d3.keys(__)),
    ),
    w = function() {
      return __.width - __.margin.right - __.margin.left;
    },
    h = function() {
      return __.height - __.margin.top - __.margin.bottom;
    },
    flags = {
      brushable: false,
      reorderable: false,
      axes: false,
      interactive: false,
      debug: false,
    },
    xscale = d3.scale.ordinal(),
    dragging = {},
    line = d3.svg.line(),
    axis = d3.svg
      .axis()
      .orient('left')
      .ticks(5),
    g, // groups for axes, brushes
    ctx = {},
    canvas = {},
    clusterCentroids = [];

  // side effects for setters
  var side_effects = d3.dispatch
    .apply(this, d3.keys(__))
    .on('composite', function(d) {
      ctx.foreground.globalCompositeOperation = d.value;
      ctx.brushed.globalCompositeOperation = d.value;
    })
    .on('alpha', function(d) {
      ctx.foreground.globalAlpha = d.value;
      ctx.brushed.globalAlpha = d.value;
    })
    .on('brushedColor', function(d) {
      ctx.brushed.strokeStyle = d.value;
    })
    .on('width', function(d) {
      pc.resize();
    })
    .on('height', function(d) {
      pc.resize();
    })
    .on('margin', function(d) {
      pc.resize();
    })
    .on('rate', function(d) {
      brushedQueue.rate(d.value);
      foregroundQueue.rate(d.value);
    })
    .on('dimensions', function(d) {
      __.dimensions = pc.applyDimensionDefaults(d3.keys(d.value));
      xscale.domain(pc.getOrderedDimensionKeys());
      pc.sortDimensions();
      if (flags.interactive) {
        pc.render().updateAxes();
      }
    })
    .on('bundleDimension', function(d) {
      if (!d3.keys(__.dimensions).length) pc.detectDimensions();
      pc.autoscale();
      if (typeof d.value === 'number') {
        if (d.value < d3.keys(__.dimensions).length) {
          __.bundleDimension = __.dimensions[d.value];
        } else if (d.value < __.hideAxis.length) {
          __.bundleDimension = __.hideAxis[d.value];
        }
      } else {
        __.bundleDimension = d.value;
      }

      __.clusterCentroids = compute_cluster_centroids(__.bundleDimension);
      if (flags.interactive) {
        pc.render();
      }
    })
    .on('hideAxis', function(d) {
      pc.dimensions(pc.applyDimensionDefaults());
      pc.dimensions(without(__.dimensions, d.value));
    })
    .on('flipAxes', function(d) {
      if (d.value && d.value.length) {
        d.value.forEach(function(axis) {
          flipAxisAndUpdatePCP(axis);
        });
        pc.updateAxes(0);
      }
    });

  // expose the state of the chart
  pc.state = __;
  pc.flags = flags;

  // create getter/setters
  getset(pc, __, events);

  // expose events
  d3.rebind(pc, events, 'on');

  // getter/setter with event firing
  function getset(obj, state, events) {
    d3.keys(state).forEach(function(key) {
      obj[key] = function(x) {
        if (!arguments.length) {
          return state[key];
        }
        if (
          key === 'dimensions' &&
          Object.prototype.toString.call(x) === '[object Array]'
        ) {
          console.warn(
            'pc.dimensions([]) is deprecated, use pc.dimensions({})',
          );
          x = pc.applyDimensionDefaults(x);
        }
        var old = state[key];
        state[key] = x;
        side_effects[key].call(pc, { value: x, previous: old });
        events[key].call(pc, { value: x, previous: old });
        return obj;
      };
    });
  }

  function extend(target, source) {
    for (var key in source) {
      target[key] = source[key];
    }
    return target;
  }

  function without(arr, items) {
    items.forEach(function(el) {
      delete arr[el];
    });
    return arr;
  }
  /** adjusts an axis' default range [h()+1, 1] if a NullValueSeparator is set */
  function getRange() {
    if (__.nullValueSeparator == 'bottom') {
      return [
        h() +
          1 -
          __.nullValueSeparatorPadding.bottom -
          __.nullValueSeparatorPadding.top,
        1,
      ];
    } else if (__.nullValueSeparator == 'top') {
      return [
        h() + 1,
        1 +
          __.nullValueSeparatorPadding.bottom +
          __.nullValueSeparatorPadding.top,
      ];
    }
    return [h() + 1, 1];
  }

  pc.autoscale = function() {
    // yscale
    var defaultScales = {
      date: function(k) {
        var extent = d3.extent(__.data, function(d) {
          return d[k] ? d[k].getTime() : null;
        });

        // special case if single value
        if (extent[0] === extent[1]) {
          return d3.scale
            .ordinal()
            .domain([extent[0]])
            .rangePoints(getRange());
        }

        return d3.time
          .scale()
          .domain(extent)
          .range(getRange());
      },
      number: function(k) {
        var extent = d3.extent(__.data, function(d) {
          return +d[k];
        });

        // special case if single value
        if (extent[0] === extent[1]) {
          return d3.scale
            .ordinal()
            .domain([extent[0]])
            .rangePoints(getRange());
        }

        return d3.scale
          .linear()
          .domain(extent)
          .range(getRange());
      },
      string: function(k) {
        var counts = {},
          domain = [];

        // Let's get the count for each value so that we can sort the domain based
        // on the number of items for each value.
        __.data.map(function(p) {
          if (p[k] === undefined && __.nullValueSeparator !== 'undefined') {
            return; // null values will be drawn beyond the horizontal null value separator!
          }
          if (counts[p[k]] === undefined) {
            counts[p[k]] = 1;
          } else {
            counts[p[k]] = counts[p[k]] + 1;
          }
        });

        domain = Object.getOwnPropertyNames(counts).sort(function(a, b) {
          return counts[a] - counts[b];
        });

        return d3.scale
          .ordinal()
          .domain(domain)
          .rangePoints(getRange());
      },
    };

    d3.keys(__.dimensions).forEach(function(k) {
      if (!__.dimensions[k].yscale) {
        __.dimensions[k].yscale = defaultScales[__.dimensions[k].type](k);
      }
    });

    // xscale
    xscale.rangePoints([0, w()], 1);

    // Retina display, etc.
    var devicePixelRatio = window.devicePixelRatio || 1;

    // canvas sizes
    pc.selection
      .selectAll('canvas')
      .style('margin-top', __.margin.top + 'px')
      .style('margin-left', __.margin.left + 'px')
      .style('width', w() + 2 + 'px')
      .style('height', h() + 2 + 'px')
      .attr('width', (w() + 2) * devicePixelRatio)
      .attr('height', (h() + 2) * devicePixelRatio);

    // default styles, needs to be set when canvas width changes
    ctx.foreground.strokeStyle = __.color;
    ctx.foreground.lineWidth = 1.4;
    ctx.foreground.globalCompositeOperation = __.composite;
    ctx.foreground.globalAlpha = __.alpha;
    ctx.foreground.scale(devicePixelRatio, devicePixelRatio);
    ctx.brushed.strokeStyle = __.brushedColor;
    ctx.brushed.lineWidth = 1.4;
    ctx.brushed.globalCompositeOperation = __.composite;
    ctx.brushed.globalAlpha = __.alpha;
    ctx.brushed.scale(devicePixelRatio, devicePixelRatio);
    ctx.highlight.lineWidth = 3;
    ctx.highlight.scale(devicePixelRatio, devicePixelRatio);

    return this;
  };

  pc.scale = function(d, domain) {
    __.dimensions[d].yscale.domain(domain);

    return this;
  };

  pc.flip = function(d) {
    //__.dimensions[d].yscale.domain().reverse();                               // does not work
    __.dimensions[d].yscale.domain(__.dimensions[d].yscale.domain().reverse()); // works

    return this;
  };

  pc.commonScale = function(global, type) {
    var t = type || 'number';
    if (typeof global === 'undefined') {
      global = true;
    }

    // try to autodetect dimensions and create scales
    if (!d3.keys(__.dimensions).length) {
      pc.detectDimensions();
    }
    pc.autoscale();

    // scales of the same type
    var scales = d3.keys(__.dimensions).filter(function(p) {
      return __.dimensions[p].type == t;
    });

    if (global) {
      var extent = d3.extent(
        scales
          .map(function(d, i) {
            return __.dimensions[d].yscale.domain();
          })
          .reduce(function(a, b) {
            return a.concat(b);
          }),
      );

      scales.forEach(function(d) {
        __.dimensions[d].yscale.domain(extent);
      });
    } else {
      scales.forEach(function(d) {
        __.dimensions[d].yscale.domain(
          d3.extent(__.data, function(d) {
            return +d[k];
          }),
        );
      });
    }

    // update centroids
    if (__.bundleDimension !== null) {
      pc.bundleDimension(__.bundleDimension);
    }

    return this;
  };
  pc.detectDimensions = function() {
    pc.dimensions(pc.applyDimensionDefaults());
    return this;
  };

  pc.applyDimensionDefaults = function(dims) {
    var types = pc.detectDimensionTypes(__.data);
    dims = dims ? dims : d3.keys(types);
    var newDims = {};
    var currIndex = 0;
    dims.forEach(function(k) {
      newDims[k] = __.dimensions[k] ? __.dimensions[k] : {};
      //Set up defaults
      newDims[k].orient = newDims[k].orient ? newDims[k].orient : 'left';
      newDims[k].ticks = newDims[k].ticks != null ? newDims[k].ticks : 5;
      newDims[k].innerTickSize =
        newDims[k].innerTickSize != null ? newDims[k].innerTickSize : 6;
      newDims[k].outerTickSize =
        newDims[k].outerTickSize != null ? newDims[k].outerTickSize : 0;
      newDims[k].tickPadding =
        newDims[k].tickPadding != null ? newDims[k].tickPadding : 3;
      newDims[k].type = newDims[k].type ? newDims[k].type : types[k];

      newDims[k].index =
        newDims[k].index != null ? newDims[k].index : currIndex;
      currIndex++;
    });
    return newDims;
  };

  pc.getOrderedDimensionKeys = function() {
    return d3.keys(__.dimensions).sort(function(x, y) {
      return d3.ascending(__.dimensions[x].index, __.dimensions[y].index);
    });
  };

  // a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable
  pc.toType = function(v) {
    return {}.toString
      .call(v)
      .match(/\s([a-zA-Z]+)/)[1]
      .toLowerCase();
  };

  // try to coerce to number before returning type
  pc.toTypeCoerceNumbers = function(v) {
    if (parseFloat(v) == v && v != null) {
      return 'number';
    }
    return pc.toType(v);
  };

  // attempt to determine types of each dimension based on first row of data
  pc.detectDimensionTypes = function(data) {
    var types = {};
    d3.keys(data[0]).forEach(function(col) {
      types[isNaN(Number(col)) ? col : parseInt(col)] = pc.toTypeCoerceNumbers(
        data[0][col],
      );
    });
    return types;
  };
  pc.render = function() {
    // try to autodetect dimensions and create scales
    if (!d3.keys(__.dimensions).length) {
      pc.detectDimensions();
    }
    pc.autoscale();

    pc.render[__.mode]();

    events.render.call(this);
    return this;
  };

  pc.renderBrushed = function() {
    if (!d3.keys(__.dimensions).length) pc.detectDimensions();

    pc.renderBrushed[__.mode]();

    events.render.call(this);
    return this;
  };

  function isBrushed() {
    if (__.brushed && __.brushed.length !== __.data.length) return true;

    var object = brush.currentMode().brushState();

    for (var key in object) {
      if (object.hasOwnProperty(key)) {
        return true;
      }
    }
    return false;
  }

  pc.render.default = function() {
    pc.clear('foreground');
    pc.clear('highlight');

    pc.renderBrushed.default();

    __.data.forEach(path_foreground);
  };

  var foregroundQueue = d3
    .renderQueue(path_foreground)
    .rate(50)
    .clear(function() {
      pc.clear('foreground');
      pc.clear('highlight');
    });

  pc.render.queue = function() {
    pc.renderBrushed.queue();

    foregroundQueue(__.data);
  };

  pc.renderBrushed.default = function() {
    pc.clear('brushed');

    if (isBrushed()) {
      __.brushed.forEach(path_brushed);
    }
  };

  var brushedQueue = d3
    .renderQueue(path_brushed)
    .rate(50)
    .clear(function() {
      pc.clear('brushed');
    });

  pc.renderBrushed.queue = function() {
    if (isBrushed()) {
      brushedQueue(__.brushed);
    } else {
      brushedQueue([]); // This is needed to clear the currently brushed items
    }
  };
  function compute_cluster_centroids(d) {
    var clusterCentroids = d3.map();
    var clusterCounts = d3.map();
    // determine clusterCounts
    __.data.forEach(function(row) {
      var scaled = __.dimensions[d].yscale(row[d]);
      if (!clusterCounts.has(scaled)) {
        clusterCounts.set(scaled, 0);
      }
      var count = clusterCounts.get(scaled);
      clusterCounts.set(scaled, count + 1);
    });

    __.data.forEach(function(row) {
      d3.keys(__.dimensions).map(function(p, i) {
        var scaled = __.dimensions[d].yscale(row[d]);
        if (!clusterCentroids.has(scaled)) {
          var map = d3.map();
          clusterCentroids.set(scaled, map);
        }
        if (!clusterCentroids.get(scaled).has(p)) {
          clusterCentroids.get(scaled).set(p, 0);
        }
        var value = clusterCentroids.get(scaled).get(p);
        value += __.dimensions[p].yscale(row[p]) / clusterCounts.get(scaled);
        clusterCentroids.get(scaled).set(p, value);
      });
    });

    return clusterCentroids;
  }

  function compute_centroids(row) {
    var centroids = [];

    var p = d3.keys(__.dimensions);
    var cols = p.length;
    var a = 0.5; // center between axes
    for (var i = 0; i < cols; ++i) {
      // centroids on 'real' axes
      var x = position(p[i]);
      var y = __.dimensions[p[i]].yscale(row[p[i]]);
      centroids.push([x, y]);

      // centroids on 'virtual' axes
      if (i < cols - 1) {
        var cx = x + a * (position(p[i + 1]) - x);
        var cy = y + a * (__.dimensions[p[i + 1]].yscale(row[p[i + 1]]) - y);
        if (__.bundleDimension !== null) {
          var leftCentroid = __.clusterCentroids
            .get(
              __.dimensions[__.bundleDimension].yscale(row[__.bundleDimension]),
            )
            .get(p[i]);
          var rightCentroid = __.clusterCentroids
            .get(
              __.dimensions[__.bundleDimension].yscale(row[__.bundleDimension]),
            )
            .get(p[i + 1]);
          var centroid = 0.5 * (leftCentroid + rightCentroid);
          cy = centroid + (1 - __.bundlingStrength) * (cy - centroid);
        }
        centroids.push([cx, cy]);
      }
    }

    return centroids;
  }

  pc.compute_real_centroids = function(row) {
    var realCentroids = [];

    var p = d3.keys(__.dimensions);
    var cols = p.length;
    var a = 0.5;

    for (var i = 0; i < cols; ++i) {
      var x = position(p[i]);
      var y = __.dimensions[p[i]].yscale(row[p[i]]);
      realCentroids.push([x, y]);
    }

    return realCentroids;
  };

  function compute_control_points(centroids) {
    var cols = centroids.length;
    var a = __.smoothness;
    var cps = [];

    cps.push(centroids[0]);
    cps.push([
      centroids[0][0] + a * 2 * (centroids[1][0] - centroids[0][0]),
      centroids[0][1],
    ]);
    for (var col = 1; col < cols - 1; ++col) {
      var mid = centroids[col];
      var left = centroids[col - 1];
      var right = centroids[col + 1];

      var diff = vectorSubtract(left, right);
      cps.push(vectorAdd(mid, vectorMultiply(diff, a)));
      cps.push(mid);
      cps.push(vectorSubtract(mid, vectorMultiply(diff, a)));
    }
    cps.push([
      centroids[cols - 1][0] +
        a * 2 * (centroids[cols - 2][0] - centroids[cols - 1][0]),
      centroids[cols - 1][1],
    ]);
    cps.push(centroids[cols - 1]);

    return cps;
  }
  pc.shadows = function() {
    flags.shadows = true;
    pc.alphaOnBrushed(0.1);
    pc.render();
    return this;
  };

  // draw dots with radius r on the axis line where data intersects
  pc.axisDots = function(r) {
    var r = r || 0.1;
    var ctx = pc.ctx.marks;
    var startAngle = 0;
    var endAngle = 2 * Math.PI;
    ctx.globalAlpha = d3.min([1 / Math.pow(__.data.length, 1 / 2), 1]);
    __.data.forEach(function(d) {
      d3.entries(__.dimensions).forEach(function(p, i) {
        ctx.beginPath();
        ctx.arc(
          position(p),
          __.dimensions[p.key].yscale(d[p]),
          r,
          startAngle,
          endAngle,
        );
        ctx.stroke();
        ctx.fill();
      });
    });
    return this;
  };

  // draw single cubic bezier curve
  function single_curve(d, ctx) {
    var centroids = compute_centroids(d);
    var cps = compute_control_points(centroids);

    ctx.moveTo(cps[0][0], cps[0][1]);
    for (var i = 1; i < cps.length; i += 3) {
      if (__.showControlPoints) {
        for (var j = 0; j < 3; j++) {
          ctx.fillRect(cps[i + j][0], cps[i + j][1], 2, 2);
        }
      }
      ctx.bezierCurveTo(
        cps[i][0],
        cps[i][1],
        cps[i + 1][0],
        cps[i + 1][1],
        cps[i + 2][0],
        cps[i + 2][1],
      );
    }
  }

  // draw single polyline
  function color_path(d, ctx) {
    ctx.beginPath();
    if (
      (__.bundleDimension !== null && __.bundlingStrength > 0) ||
      __.smoothness > 0
    ) {
      single_curve(d, ctx);
    } else {
      single_path(d, ctx);
    }
    ctx.stroke();
  }

  // draw many polylines of the same color
  function paths(data, ctx) {
    ctx.clearRect(-1, -1, w() + 2, h() + 2);
    ctx.beginPath();
    data.forEach(function(d) {
      if (
        (__.bundleDimension !== null && __.bundlingStrength > 0) ||
        __.smoothness > 0
      ) {
        single_curve(d, ctx);
      } else {
        single_path(d, ctx);
      }
    });
    ctx.stroke();
  }

  // returns the y-position just beyond the separating null value line
  function getNullPosition() {
    if (__.nullValueSeparator == 'bottom') {
      return h() + 1;
    } else if (__.nullValueSeparator == 'top') {
      return 1;
    } else {
      console.log(
        "A value is NULL, but nullValueSeparator is not set; set it to 'bottom' or 'top'.",
      );
    }
    return h() + 1;
  }

  function single_path(d, ctx) {
    d3.entries(__.dimensions).forEach(function(p, i) {
      //p isn't really p
      if (i == 0) {
        ctx.moveTo(
          position(p.key),
          typeof d[p.key] == 'undefined'
            ? getNullPosition()
            : __.dimensions[p.key].yscale(d[p.key]),
        );
      } else {
        ctx.lineTo(
          position(p.key),
          typeof d[p.key] == 'undefined'
            ? getNullPosition()
            : __.dimensions[p.key].yscale(d[p.key]),
        );
      }
    });
  }

  function path_brushed(d, i) {
    if (__.brushedColor !== null) {
      ctx.brushed.strokeStyle = d3.functor(__.brushedColor)(d, i);
    } else {
      ctx.brushed.strokeStyle = d3.functor(__.color)(d, i);
    }
    return color_path(d, ctx.brushed);
  }

  function path_foreground(d, i) {
    ctx.foreground.strokeStyle = d3.functor(__.color)(d, i);
    return color_path(d, ctx.foreground);
  }

  function path_highlight(d, i) {
    ctx.highlight.strokeStyle = d3.functor(__.color)(d, i);
    return color_path(d, ctx.highlight);
  }
  pc.clear = function(layer) {
    ctx[layer].clearRect(0, 0, w() + 2, h() + 2);

    // This will make sure that the foreground items are transparent
    // without the need for changing the opacity style of the foreground canvas
    // as this would stop the css styling from working
    if (layer === 'brushed' && isBrushed()) {
      ctx.brushed.fillStyle = pc.selection.style('background-color');
      ctx.brushed.globalAlpha = 1 - __.alphaOnBrushed;
      ctx.brushed.fillRect(0, 0, w() + 2, h() + 2);
      ctx.brushed.globalAlpha = __.alpha;
    }
    return this;
  };
  d3.rebind(
    pc,
    axis,
    'ticks',
    'orient',
    'tickValues',
    'tickSubdivide',
    'tickSize',
    'tickPadding',
    'tickFormat',
  );

  function flipAxisAndUpdatePCP(dimension) {
    var g = pc.svg.selectAll('.dimension');

    pc.flip(dimension);

    d3.select(this.parentElement)
      .transition()
      .duration(__.animationTime)
      .call(pc.applyAxisConfig(axis, __.dimensions[dimension]))
      .call(axis.orient(__.dimensions[dimension].orient))
      .call(axis.ticks(__.dimensions[dimension].ticks))
      .call(axis.innerTickSize(__.dimensions[dimension].innerTickSize))
      .call(axis.outerTickSize(__.dimensions[dimension].outerTickSize))
      .call(axis.tickPadding(__.dimensions[dimension].tickPadding))
      .call(axis.tickFormat(__.dimensions[dimension].tickFormat));

    pc.render();
  }

  function rotateLabels() {
    if (!__.rotateLabels) return;

    var delta = d3.event.deltaY;
    delta = delta < 0 ? -5 : delta;
    delta = delta > 0 ? 5 : delta;

    __.dimensionTitleRotation += delta;
    pc.svg
      .selectAll('text.label')
      .attr(
        'transform',
        'translate(0,-5) rotate(' + __.dimensionTitleRotation + ')',
      );
    d3.event.preventDefault();
  }

  function dimensionLabels(d) {
    return __.dimensions[d].title ? __.dimensions[d].title : d; // dimension display names
  }

  pc.createAxes = function() {
    if (g) pc.removeAxes();

    // Add a group element for each dimension.
    g = pc.svg
      .selectAll('.dimension')
      .data(pc.getOrderedDimensionKeys(), function(d) {
        return d;
      })
      .enter()
      .append('svg:g')
      .attr('class', 'dimension')
      .attr('transform', function(d) {
        return 'translate(' + xscale(d) + ')';
      });

    // Add an axis and title.
    g.append('svg:g')
      .attr('class', 'axis')
      .attr('transform', 'translate(0,0)')
      .each(function(d) {
        var axisElement = d3
          .select(this)
          .call(pc.applyAxisConfig(axis, __.dimensions[d]));

        axisElement
          .selectAll('path')
          .style('fill', 'none')
          .style('stroke', '#222')
          .style('shape-rendering', 'crispEdges');

        axisElement
          .selectAll('line')
          .style('fill', 'none')
          .style('stroke', '#222')
          .style('shape-rendering', 'crispEdges');
      })
      .append('svg:text')
      .attr({
        'text-anchor': 'middle',
        y: 0,
        transform: 'translate(0,-5) rotate(' + __.dimensionTitleRotation + ')',
        x: 0,
        class: 'label',
      })
      .text(dimensionLabels)
      .on('dblclick', flipAxisAndUpdatePCP)
      .on('wheel', rotateLabels);

    if (__.nullValueSeparator == 'top') {
      pc.svg
        .append('line')
        .attr('x1', 0)
        .attr('y1', 1 + __.nullValueSeparatorPadding.top)
        .attr('x2', w())
        .attr('y2', 1 + __.nullValueSeparatorPadding.top)
        .attr('stroke-width', 1)
        .attr('stroke', '#777')
        .attr('fill', 'none')
        .attr('shape-rendering', 'crispEdges');
    } else if (__.nullValueSeparator == 'bottom') {
      pc.svg
        .append('line')
        .attr('x1', 0)
        .attr('y1', h() + 1 - __.nullValueSeparatorPadding.bottom)
        .attr('x2', w())
        .attr('y2', h() + 1 - __.nullValueSeparatorPadding.bottom)
        .attr('stroke-width', 1)
        .attr('stroke', '#777')
        .attr('fill', 'none')
        .attr('shape-rendering', 'crispEdges');
    }

    flags.axes = true;
    return this;
  };

  pc.removeAxes = function() {
    g.remove();
    g = undefined;
    return this;
  };

  pc.updateAxes = function(animationTime) {
    if (typeof animationTime === 'undefined') {
      animationTime = __.animationTime;
    }

    var g_data = pc.svg
      .selectAll('.dimension')
      .data(pc.getOrderedDimensionKeys());

    // Enter
    g_data
      .enter()
      .append('svg:g')
      .attr('class', 'dimension')
      .attr('transform', function(p) {
        return 'translate(' + position(p) + ')';
      })
      .style('opacity', 0)
      .append('svg:g')
      .attr('class', 'axis')
      .attr('transform', 'translate(0,0)')
      .each(function(d) {
        var axisElement = d3
          .select(this)
          .call(pc.applyAxisConfig(axis, __.dimensions[d]));

        axisElement
          .selectAll('path')
          .style('fill', 'none')
          .style('stroke', '#222')
          .style('shape-rendering', 'crispEdges');

        axisElement
          .selectAll('line')
          .style('fill', 'none')
          .style('stroke', '#222')
          .style('shape-rendering', 'crispEdges');
      })
      .append('svg:text')
      .attr({
        'text-anchor': 'middle',
        y: 0,
        transform: 'translate(0,-5) rotate(' + __.dimensionTitleRotation + ')',
        x: 0,
        class: 'label',
      })
      .text(dimensionLabels)
      .on('dblclick', flipAxisAndUpdatePCP)
      .on('wheel', rotateLabels);

    // Update
    g_data.attr('opacity', 0);
    g_data
      .select('.axis')
      .transition()
      .duration(animationTime)
      .each(function(d) {
        d3.select(this).call(pc.applyAxisConfig(axis, __.dimensions[d]));
      });
    g_data
      .select('.label')
      .transition()
      .duration(animationTime)
      .text(dimensionLabels)
      .attr(
        'transform',
        'translate(0,-5) rotate(' + __.dimensionTitleRotation + ')',
      );

    // Exit
    g_data.exit().remove();

    g = pc.svg.selectAll('.dimension');
    g.transition()
      .duration(animationTime)
      .attr('transform', function(p) {
        return 'translate(' + position(p) + ')';
      })
      .style('opacity', 1);

    pc.svg
      .selectAll('.axis')
      .transition()
      .duration(animationTime)
      .each(function(d) {
        d3.select(this).call(pc.applyAxisConfig(axis, __.dimensions[d]));
      });

    if (flags.brushable) pc.brushable();
    if (flags.reorderable) pc.reorderable();
    if (pc.brushMode() !== 'None') {
      var mode = pc.brushMode();
      pc.brushMode('None');
      pc.brushMode(mode);
    }
    return this;
  };

  pc.applyAxisConfig = function(axis, dimension) {
    return axis
      .scale(dimension.yscale)
      .orient(dimension.orient)
      .ticks(dimension.ticks)
      .tickValues(dimension.tickValues)
      .innerTickSize(dimension.innerTickSize)
      .outerTickSize(dimension.outerTickSize)
      .tickPadding(dimension.tickPadding)
      .tickFormat(dimension.tickFormat);
  };

  // Jason Davies, http://bl.ocks.org/1341281
  pc.reorderable = function() {
    if (!g) pc.createAxes();

    g.style('cursor', 'move').call(
      d3.behavior
        .drag()
        .on('dragstart', function(d) {
          dragging[d] = this.__origin__ = xscale(d);
        })
        .on('drag', function(d) {
          dragging[d] = Math.min(
            w(),
            Math.max(0, (this.__origin__ += d3.event.dx)),
          );
          pc.sortDimensions();
          xscale.domain(pc.getOrderedDimensionKeys());
          pc.render();
          g.attr('transform', function(d) {
            return 'translate(' + position(d) + ')';
          });
        })
        .on('dragend', function(d) {
          // Let's see if the order has changed and send out an event if so.
          var i = 0,
            j = __.dimensions[d].index,
            elem = this,
            parent = this.parentElement;

          while ((elem = elem.previousElementSibling) != null) ++i;
          if (i !== j) {
            events.axesreorder.call(pc, pc.getOrderedDimensionKeys());
            // We now also want to reorder the actual dom elements that represent
            // the axes. That is, the g.dimension elements. If we don't do this,
            // we get a weird and confusing transition when updateAxes is called.
            // This is due to the fact that, initially the nth g.dimension element
            // represents the nth axis. However, after a manual reordering,
            // without reordering the dom elements, the nth dom elements no longer
            // necessarily represents the nth axis.
            //
            // i is the original index of the dom element
            // j is the new index of the dom element
            if (i > j) {
              // Element moved left
              parent.insertBefore(this, parent.children[j - 1]);
            } else {
              // Element moved right
              if (j + 1 < parent.children.length) {
                parent.insertBefore(this, parent.children[j + 1]);
              } else {
                parent.appendChild(this);
              }
            }
          }

          delete this.__origin__;
          delete dragging[d];
          d3.select(this)
            .transition()
            .attr('transform', 'translate(' + xscale(d) + ')');
          pc.render();
        }),
    );
    flags.reorderable = true;
    return this;
  };

  // Reorder dimensions, such that the highest value (visually) is on the left and
  // the lowest on the right. Visual values are determined by the data values in
  // the given row.
  pc.reorder = function(rowdata) {
    var firstDim = pc.getOrderedDimensionKeys()[0];

    pc.sortDimensionsByRowData(rowdata);
    // NOTE: this is relatively cheap given that:
    // number of dimensions < number of data items
    // Thus we check equality of order to prevent rerendering when this is the case.
    var reordered = false;
    reordered = firstDim !== pc.getOrderedDimensionKeys()[0];

    if (reordered) {
      xscale.domain(pc.getOrderedDimensionKeys());
      var highlighted = __.highlighted.slice(0);
      pc.unhighlight();

      g.transition()
        .duration(1500)
        .attr('transform', function(d) {
          return 'translate(' + xscale(d) + ')';
        });
      pc.render();

      // pc.highlight() does not check whether highlighted is length zero, so we do that here.
      if (highlighted.length !== 0) {
        pc.highlight(highlighted);
      }
    }
  };

  pc.sortDimensionsByRowData = function(rowdata) {
    var copy = __.dimensions;
    var positionSortedKeys = d3.keys(__.dimensions).sort(function(a, b) {
      var pixelDifference =
        __.dimensions[a].yscale(rowdata[a]) -
        __.dimensions[b].yscale(rowdata[b]);

      // Array.sort is not necessarily stable, this means that if pixelDifference is zero
      // the ordering of dimensions might change unexpectedly. This is solved by sorting on
      // variable name in that case.
      if (pixelDifference === 0) {
        return a.localeCompare(b);
      } // else
      return pixelDifference;
    });
    __.dimensions = {};
    positionSortedKeys.forEach(function(p, i) {
      __.dimensions[p] = copy[p];
      __.dimensions[p].index = i;
    });
  };

  pc.sortDimensions = function() {
    var copy = __.dimensions;
    var positionSortedKeys = d3.keys(__.dimensions).sort(function(a, b) {
      return position(a) - position(b);
    });
    __.dimensions = {};
    positionSortedKeys.forEach(function(p, i) {
      __.dimensions[p] = copy[p];
      __.dimensions[p].index = i;
    });
  };

  // pairs of adjacent dimensions
  pc.adjacent_pairs = function(arr) {
    var ret = [];
    for (var i = 0; i < arr.length - 1; i++) {
      ret.push([arr[i], arr[i + 1]]);
    }
    return ret;
  };

  var brush = {
    modes: {
      None: {
        install: function(pc) {}, // Nothing to be done.
        uninstall: function(pc) {}, // Nothing to be done.
        selected: function() {
          return [];
        }, // Nothing to return
        brushState: function() {
          return {};
        },
      },
    },
    mode: 'None',
    predicate: 'AND',
    currentMode: function() {
      return this.modes[this.mode];
    },
  };

  // This function can be used for 'live' updates of brushes. That is, during the
  // specification of a brush, this method can be called to update the view.
  //
  // @param newSelection - The new set of data items that is currently contained
  //                       by the brushes
  function brushUpdated(newSelection) {
    __.brushed = newSelection;
    events.brush.call(pc, __.brushed);
    pc.renderBrushed();
  }

  function brushPredicate(predicate) {
    if (!arguments.length) {
      return brush.predicate;
    }

    predicate = String(predicate).toUpperCase();
    if (predicate !== 'AND' && predicate !== 'OR') {
      throw 'Invalid predicate ' + predicate;
    }

    brush.predicate = predicate;
    __.brushed = brush.currentMode().selected();
    pc.renderBrushed();
    return pc;
  }

  pc.brushModes = function() {
    return Object.getOwnPropertyNames(brush.modes);
  };

  pc.brushMode = function(mode) {
    if (arguments.length === 0) {
      return brush.mode;
    }

    if (pc.brushModes().indexOf(mode) === -1) {
      throw 'pc.brushmode: Unsupported brush mode: ' + mode;
    }

    // Make sure that we don't trigger unnecessary events by checking if the mode
    // actually changes.
    if (mode !== brush.mode) {
      // When changing brush modes, the first thing we need to do is clearing any
      // brushes from the current mode, if any.
      if (brush.mode !== 'None') {
        pc.brushReset();
      }

      // Next, we need to 'uninstall' the current brushMode.
      brush.modes[brush.mode].uninstall(pc);
      // Finally, we can install the requested one.
      brush.mode = mode;
      brush.modes[brush.mode].install();
      if (mode === 'None') {
        delete pc.brushPredicate;
      } else {
        pc.brushPredicate = brushPredicate;
      }
    }

    return pc;
  };

  // brush mode: 1D-Axes

  (function() {
    var brushes = {};

    function is_brushed(p) {
      return !brushes[p].empty();
    }

    // data within extents
    function selected() {
      var actives = d3.keys(__.dimensions).filter(is_brushed),
        extents = actives.map(function(p) {
          return brushes[p].extent();
        });

      // We don't want to return the full data set when there are no axes brushed.
      // Actually, when there are no axes brushed, by definition, no items are
      // selected. So, let's avoid the filtering and just return false.
      //if (actives.length === 0) return false;

      // Resolves broken examples for now. They expect to get the full dataset back from empty brushes
      if (actives.length === 0) return __.data;

      // test if within range
      var within = {
        date: function(d, p, dimension) {
          if (typeof __.dimensions[p].yscale.rangePoints === 'function') {
            // if it is ordinal
            return (
              extents[dimension][0] <= __.dimensions[p].yscale(d[p]) &&
              __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
            );
          } else {
            return (
              extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
            );
          }
        },
        number: function(d, p, dimension) {
          if (typeof __.dimensions[p].yscale.rangePoints === 'function') {
            // if it is ordinal
            return (
              extents[dimension][0] <= __.dimensions[p].yscale(d[p]) &&
              __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
            );
          } else {
            return (
              extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
            );
          }
        },
        string: function(d, p, dimension) {
          return (
            extents[dimension][0] <= __.dimensions[p].yscale(d[p]) &&
            __.dimensions[p].yscale(d[p]) <= extents[dimension][1]
          );
        },
      };

      return __.data.filter(function(d) {
        switch (brush.predicate) {
          case 'AND':
            return actives.every(function(p, dimension) {
              return within[__.dimensions[p].type](d, p, dimension);
            });
          case 'OR':
            return actives.some(function(p, dimension) {
              return within[__.dimensions[p].type](d, p, dimension);
            });
          default:
            throw 'Unknown brush predicate ' + __.brushPredicate;
        }
      });
    }

    function brushExtents(extents) {
      if (typeof extents === 'undefined') {
        var extents = {};
        d3.keys(__.dimensions).forEach(function(d) {
          var brush = brushes[d];
          if (brush !== undefined && !brush.empty()) {
            var extent = brush.extent();
            extent.sort(d3.ascending);
            extents[d] = extent;
          }
        });
        return extents;
      } else {
        //first get all the brush selections
        var brushSelections = {};
        g.selectAll('.brush').each(function(d) {
          brushSelections[d] = d3.select(this);
        });

        // loop over each dimension and update appropriately (if it was passed in through extents)
        d3.keys(__.dimensions).forEach(function(d) {
          if (extents[d] === undefined) {
            return;
          }

          var brush = brushes[d];
          if (brush !== undefined) {
            //update the extent
            brush.extent(extents[d]);

            //redraw the brush
            brushSelections[d]
              .transition()
              .duration(0)
              .call(brush);

            //fire some events
            brush.event(brushSelections[d]);
          }
        });

        //redraw the chart
        pc.renderBrushed();

        return pc;
      }
    }

    function brushFor(axis) {
      var brush = d3.svg.brush();

      brush
        .y(__.dimensions[axis].yscale)
        .on('brushstart', function() {
          if (d3.event.sourceEvent !== null) {
            events.brushstart.call(pc, __.brushed);
            d3.event.sourceEvent.stopPropagation();
          }
        })
        .on('brush', function() {
          brushUpdated(selected());
        })
        .on('brushend', function() {
          events.brushend.call(pc, __.brushed);
        });

      brushes[axis] = brush;
      return brush;
    }

    function brushReset(dimension) {
      if (dimension === undefined) {
        __.brushed = false;
        if (g) {
          g.selectAll('.brush').each(function(d) {
            d3.select(this)
              .transition()
              .duration(0)
              .call(brushes[d].clear());
          });
          pc.renderBrushed();
        }
      } else {
        if (g) {
          g.selectAll('.brush').each(function(d) {
            if (d != dimension) return;
            d3.select(this)
              .transition()
              .duration(0)
              .call(brushes[d].clear());
            brushes[d].event(d3.select(this));
          });
          pc.renderBrushed();
        }
      }
      return this;
    }

    function install() {
      if (!g) pc.createAxes();

      // Add and store a brush for each axis.
      var brush = g
        .append('svg:g')
        .attr('class', 'brush')
        .each(function(d) {
          d3.select(this).call(brushFor(d));
        });

      brush
        .selectAll('rect')
        .style('visibility', null)
        .attr('x', -15)
        .attr('width', 30);

      brush.selectAll('rect.background').style('fill', 'transparent');

      brush
        .selectAll('rect.extent')
        .style('fill', 'rgba(255,255,255,0.25)')
        .style('stroke', 'rgba(0,0,0,0.6)');

      brush.selectAll('.resize rect').style('fill', 'rgba(0,0,0,0.1)');

      pc.brushExtents = brushExtents;
      pc.brushReset = brushReset;
      return pc;
    }

    brush.modes['1D-axes'] = {
      install: install,
      uninstall: function() {
        g.selectAll('.brush').remove();
        brushes = {};
        delete pc.brushExtents;
        delete pc.brushReset;
      },
      selected: selected,
      brushState: brushExtents,
    };
  })();
  // brush mode: 2D-strums
  // bl.ocks.org/syntagmatic/5441022

  (function() {
    var strums = {},
      strumRect;

    function drawStrum(strum, activePoint) {
      var svg = pc.selection.select('svg').select('g#strums'),
        id = strum.dims.i,
        points = [strum.p1, strum.p2],
        line = svg.selectAll('line#strum-' + id).data([strum]),
        circles = svg.selectAll('circle#strum-' + id).data(points),
        drag = d3.behavior.drag();

      line
        .enter()
        .append('line')
        .attr('id', 'strum-' + id)
        .attr('class', 'strum');

      line
        .attr('x1', function(d) {
          return d.p1[0];
        })
        .attr('y1', function(d) {
          return d.p1[1];
        })
        .attr('x2', function(d) {
          return d.p2[0];
        })
        .attr('y2', function(d) {
          return d.p2[1];
        })
        .attr('stroke', 'black')
        .attr('stroke-width', 2);

      drag
        .on('drag', function(d, i) {
          var ev = d3.event;
          i = i + 1;
          strum['p' + i][0] = Math.min(
            Math.max(strum.minX + 1, ev.x),
            strum.maxX,
          );
          strum['p' + i][1] = Math.min(Math.max(strum.minY, ev.y), strum.maxY);
          drawStrum(strum, i - 1);
        })
        .on('dragend', onDragEnd());

      circles
        .enter()
        .append('circle')
        .attr('id', 'strum-' + id)
        .attr('class', 'strum');

      circles
        .attr('cx', function(d) {
          return d[0];
        })
        .attr('cy', function(d) {
          return d[1];
        })
        .attr('r', 5)
        .style('opacity', function(d, i) {
          return activePoint !== undefined && i === activePoint ? 0.8 : 0;
        })
        .on('mouseover', function() {
          d3.select(this).style('opacity', 0.8);
        })
        .on('mouseout', function() {
          d3.select(this).style('opacity', 0);
        })
        .call(drag);
    }

    function dimensionsForPoint(p) {
      var dims = { i: -1, left: undefined, right: undefined };
      d3.keys(__.dimensions).some(function(dim, i) {
        if (xscale(dim) < p[0]) {
          var next = d3.keys(__.dimensions)[
            pc.getOrderedDimensionKeys().indexOf(dim) + 1
          ];
          dims.i = i;
          dims.left = dim;
          dims.right = next;
          return false;
        }
        return true;
      });

      if (dims.left === undefined) {
        // Event on the left side of the first axis.
        dims.i = 0;
        dims.left = pc.getOrderedDimensionKeys()[0];
        dims.right = pc.getOrderedDimensionKeys()[1];
      } else if (dims.right === undefined) {
        // Event on the right side of the last axis
        dims.i = d3.keys(__.dimensions).length - 1;
        dims.right = dims.left;
        dims.left = pc.getOrderedDimensionKeys()[
          d3.keys(__.dimensions).length - 2
        ];
      }

      return dims;
    }

    function onDragStart() {
      // First we need to determine between which two axes the sturm was started.
      // This will determine the freedom of movement, because a strum can
      // logically only happen between two axes, so no movement outside these axes
      // should be allowed.
      return function() {
        var p = d3.mouse(strumRect[0][0]),
          dims,
          strum;

        p[0] = p[0] - __.margin.left;
        p[1] = p[1] - __.margin.top;

        (dims = dimensionsForPoint(p)),
          (strum = {
            p1: p,
            dims: dims,
            minX: xscale(dims.left),
            maxX: xscale(dims.right),
            minY: 0,
            maxY: h(),
          });

        strums[dims.i] = strum;
        strums.active = dims.i;

        // Make sure that the point is within the bounds
        strum.p1[0] = Math.min(Math.max(strum.minX, p[0]), strum.maxX);
        strum.p2 = strum.p1.slice();
      };
    }

    function onDrag() {
      return function() {
        var ev = d3.event,
          strum = strums[strums.active];

        // Make sure that the point is within the bounds
        strum.p2[0] = Math.min(
          Math.max(strum.minX + 1, ev.x - __.margin.left),
          strum.maxX,
        );
        strum.p2[1] = Math.min(
          Math.max(strum.minY, ev.y - __.margin.top),
          strum.maxY,
        );
        drawStrum(strum, 1);
      };
    }

    function containmentTest(strum, width) {
      var p1 = [strum.p1[0] - strum.minX, strum.p1[1] - strum.minX],
        p2 = [strum.p2[0] - strum.minX, strum.p2[1] - strum.minX],
        m1 = 1 - width / p1[0],
        b1 = p1[1] * (1 - m1),
        m2 = 1 - width / p2[0],
        b2 = p2[1] * (1 - m2);

      // test if point falls between lines
      return function(p) {
        var x = p[0],
          y = p[1],
          y1 = m1 * x + b1,
          y2 = m2 * x + b2;

        if (y > Math.min(y1, y2) && y < Math.max(y1, y2)) {
          return true;
        }

        return false;
      };
    }

    function selected() {
      var ids = Object.getOwnPropertyNames(strums),
        brushed = __.data;

      // Get the ids of the currently active strums.
      ids = ids.filter(function(d) {
        return !isNaN(d);
      });

      function crossesStrum(d, id) {
        var strum = strums[id],
          test = containmentTest(strum, strums.width(id)),
          d1 = strum.dims.left,
          d2 = strum.dims.right,
          y1 = __.dimensions[d1].yscale,
          y2 = __.dimensions[d2].yscale,
          point = [y1(d[d1]) - strum.minX, y2(d[d2]) - strum.minX];
        return test(point);
      }

      if (ids.length === 0) {
        return brushed;
      }

      return brushed.filter(function(d) {
        switch (brush.predicate) {
          case 'AND':
            return ids.every(function(id) {
              return crossesStrum(d, id);
            });
          case 'OR':
            return ids.some(function(id) {
              return crossesStrum(d, id);
            });
          default:
            throw 'Unknown brush predicate ' + __.brushPredicate;
        }
      });
    }

    function removeStrum() {
      var strum = strums[strums.active],
        svg = pc.selection.select('svg').select('g#strums');

      delete strums[strums.active];
      strums.active = undefined;
      svg.selectAll('line#strum-' + strum.dims.i).remove();
      svg.selectAll('circle#strum-' + strum.dims.i).remove();
    }

    function onDragEnd() {
      return function() {
        var brushed = __.data,
          strum = strums[strums.active];

        // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
        // considered a drag without move. So we have to deal with that case
        if (
          strum &&
          strum.p1[0] === strum.p2[0] &&
          strum.p1[1] === strum.p2[1]
        ) {
          removeStrum(strums);
        }

        brushed = selected(strums);
        strums.active = undefined;
        __.brushed = brushed;
        pc.renderBrushed();
        events.brushend.call(pc, __.brushed);
      };
    }

    function brushReset(strums) {
      return function() {
        var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
          return !isNaN(d);
        });

        ids.forEach(function(d) {
          strums.active = d;
          removeStrum(strums);
        });
        onDragEnd(strums)();
      };
    }

    function install() {
      var drag = d3.behavior.drag();

      // Map of current strums. Strums are stored per segment of the PC. A segment,
      // being the area between two axes. The left most area is indexed at 0.
      strums.active = undefined;
      // Returns the width of the PC segment where currently a strum is being
      // placed. NOTE: even though they are evenly spaced in our current
      // implementation, we keep for when non-even spaced segments are supported as
      // well.
      strums.width = function(id) {
        var strum = strums[id];

        if (strum === undefined) {
          return undefined;
        }

        return strum.maxX - strum.minX;
      };

      pc.on('axesreorder.strums', function() {
        var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
          return !isNaN(d);
        });

        // Checks if the first dimension is directly left of the second dimension.
        function consecutive(first, second) {
          var length = d3.keys(__.dimensions).length;
          return d3.keys(__.dimensions).some(function(d, i) {
            return d === first
              ? i + i < length && __.dimensions[i + 1] === second
              : false;
          });
        }

        if (ids.length > 0) {
          // We have some strums, which might need to be removed.
          ids.forEach(function(d) {
            var dims = strums[d].dims;
            strums.active = d;
            // If the two dimensions of the current strum are not next to each other
            // any more, than we'll need to remove the strum. Otherwise we keep it.
            if (!consecutive(dims.left, dims.right)) {
              removeStrum(strums);
            }
          });
          onDragEnd(strums)();
        }
      });

      // Add a new svg group in which we draw the strums.
      pc.selection
        .select('svg')
        .append('g')
        .attr('id', 'strums')
        .attr(
          'transform',
          'translate(' + __.margin.left + ',' + __.margin.top + ')',
        );

      // Install the required brushReset function
      pc.brushReset = brushReset(strums);

      drag
        .on('dragstart', onDragStart(strums))
        .on('drag', onDrag(strums))
        .on('dragend', onDragEnd(strums));

      // NOTE: The styling needs to be done here and not in the css. This is because
      //       for 1D brushing, the canvas layers should not listen to
      //       pointer-events.
      strumRect = pc.selection
        .select('svg')
        .insert('rect', 'g#strums')
        .attr('id', 'strum-events')
        .attr('x', __.margin.left)
        .attr('y', __.margin.top)
        .attr('width', w())
        .attr('height', h() + 2)
        .style('opacity', 0)
        .call(drag);
    }

    brush.modes['2D-strums'] = {
      install: install,
      uninstall: function() {
        pc.selection
          .select('svg')
          .select('g#strums')
          .remove();
        pc.selection
          .select('svg')
          .select('rect#strum-events')
          .remove();
        pc.on('axesreorder.strums', undefined);
        delete pc.brushReset;

        strumRect = undefined;
      },
      selected: selected,
      brushState: function() {
        return strums;
      },
    };
  })();

  // brush mode: 1D-Axes with multiple extents
  // requires d3.svg.multibrush

  (function() {
    if (typeof d3.svg.multibrush !== 'function') {
      return;
    }
    var brushes = {};

    function is_brushed(p) {
      return !brushes[p].empty();
    }

    // data within extents
    function selected() {
      var actives = d3.keys(__.dimensions).filter(is_brushed),
        extents = actives.map(function(p) {
          return brushes[p].extent();
        });

      // We don't want to return the full data set when there are no axes brushed.
      // Actually, when there are no axes brushed, by definition, no items are
      // selected. So, let's avoid the filtering and just return false.
      //if (actives.length === 0) return false;

      // Resolves broken examples for now. They expect to get the full dataset back from empty brushes
      if (actives.length === 0) return __.data;

      // test if within range
      var within = {
        date: function(d, p, dimension, b) {
          if (typeof __.dimensions[p].yscale.rangePoints === 'function') {
            // if it is ordinal
            return (
              b[0] <= __.dimensions[p].yscale(d[p]) &&
              __.dimensions[p].yscale(d[p]) <= b[1]
            );
          } else {
            return b[0] <= d[p] && d[p] <= b[1];
          }
        },
        number: function(d, p, dimension, b) {
          if (typeof __.dimensions[p].yscale.rangePoints === 'function') {
            // if it is ordinal
            return (
              b[0] <= __.dimensions[p].yscale(d[p]) &&
              __.dimensions[p].yscale(d[p]) <= b[1]
            );
          } else {
            return b[0] <= d[p] && d[p] <= b[1];
          }
        },
        string: function(d, p, dimension, b) {
          return (
            b[0] <= __.dimensions[p].yscale(d[p]) &&
            __.dimensions[p].yscale(d[p]) <= b[1]
          );
        },
      };

      return __.data.filter(function(d) {
        switch (brush.predicate) {
          case 'AND':
            return actives.every(function(p, dimension) {
              return extents[dimension].some(function(b) {
                return within[__.dimensions[p].type](d, p, dimension, b);
              });
            });
          case 'OR':
            return actives.some(function(p, dimension) {
              return extents[dimension].some(function(b) {
                return within[__.dimensions[p].type](d, p, dimension, b);
              });
            });
          default:
            throw 'Unknown brush predicate ' + __.brushPredicate;
        }
      });
    }

    function brushExtents(extents) {
      if (typeof extents === 'undefined') {
        extents = {};
        d3.keys(__.dimensions).forEach(function(d) {
          var brush = brushes[d];
          if (brush !== undefined && !brush.empty()) {
            var extent = brush.extent();
            extents[d] = extent;
          }
        });
        return extents;
      } else {
        //first get all the brush selections
        var brushSelections = {};
        g.selectAll('.brush').each(function(d) {
          brushSelections[d] = d3.select(this);
        });

        // loop over each dimension and update appropriately (if it was passed in through extents)
        d3.keys(__.dimensions).forEach(function(d) {
          if (extents[d] === undefined) {
            return;
          }

          var brush = brushes[d];
          if (brush !== undefined) {
            //update the extent
            brush.extent(extents[d]);

            //redraw the brush
            brushSelections[d]
              .transition()
              .duration(0)
              .call(brush);

            //fire some events
            brush.event(brushSelections[d]);
          }
        });

        //redraw the chart
        pc.renderBrushed();

        return pc;
      }
    }

    //function brushExtents() {
    //  var extents = {};
    //  d3.keys(__.dimensions).forEach(function(d) {
    //    var brush = brushes[d];
    //    if (brush !== undefined && !brush.empty()) {
    //      var extent = brush.extent();
    //      extents[d] = extent;
    //    }
    //  });
    //  return extents;
    //}

    function brushFor(axis) {
      var brush = d3.svg.multibrush();

      brush
        .y(__.dimensions[axis].yscale)
        .on('brushstart', function() {
          if (d3.event.sourceEvent !== null) {
            events.brushstart.call(pc, __.brushed);
            d3.event.sourceEvent.stopPropagation();
          }
        })
        .on('brush', function() {
          brushUpdated(selected());
        })
        .on('brushend', function() {
          // d3.svg.multibrush clears extents just before calling 'brushend'
          // so we have to update here again.
          // This fixes issue #103 for now, but should be changed in d3.svg.multibrush
          // to avoid unnecessary computation.
          brushUpdated(selected());
          events.brushend.call(pc, __.brushed);
        })
        .extentAdaption(function(selection) {
          selection
            .style('visibility', null)
            .attr('x', -15)
            .attr('width', 30)
            .style('fill', 'rgba(255,255,255,0.25)')
            .style('stroke', 'rgba(0,0,0,0.6)');
        })
        .resizeAdaption(function(selection) {
          selection
            .selectAll('rect')
            .attr('x', -15)
            .attr('width', 30)
            .style('visibility', null)
            .style('fill', 'rgba(0,0,0,0.1)');
        });

      brushes[axis] = brush;
      return brush;
    }

    function brushReset(dimension) {
      __.brushed = false;
      if (g) {
        g.selectAll('.brush').each(function(d) {
          d3.select(this).call(brushes[d].clear());
        });
        pc.renderBrushed();
      }
      return this;
    }

    function install() {
      if (!g) pc.createAxes();

      // Add and store a brush for each axis.
      var brush = g
        .append('svg:g')
        .attr('class', 'brush')
        .each(function(d) {
          d3.select(this).call(brushFor(d));
        });

      brush
        .selectAll('rect')
        .style('visibility', null)
        .attr('x', -15)
        .attr('width', 30);

      brush.selectAll('rect.background').style('fill', 'transparent');

      brush
        .selectAll('rect.extent')
        .style('fill', 'rgba(255,255,255,0.25)')
        .style('stroke', 'rgba(0,0,0,0.6)');

      brush.selectAll('.resize rect').style('fill', 'rgba(0,0,0,0.1)');

      pc.brushExtents = brushExtents;
      pc.brushReset = brushReset;
      return pc;
    }

    brush.modes['1D-axes-multi'] = {
      install: install,
      uninstall: function() {
        g.selectAll('.brush').remove();
        brushes = {};
        delete pc.brushExtents;
        delete pc.brushReset;
      },
      selected: selected,
      brushState: brushExtents,
    };
  })();
  // brush mode: angular
  // code based on 2D.strums.js

  (function() {
    var arcs = {},
      strumRect;

    function drawStrum(arc, activePoint) {
      var svg = pc.selection.select('svg').select('g#arcs'),
        id = arc.dims.i,
        points = [arc.p2, arc.p3],
        line = svg.selectAll('line#arc-' + id).data([
          { p1: arc.p1, p2: arc.p2 },
          { p1: arc.p1, p2: arc.p3 },
        ]),
        circles = svg.selectAll('circle#arc-' + id).data(points),
        drag = d3.behavior.drag(),
        path = svg.selectAll('path#arc-' + id).data([arc]);

      path
        .enter()
        .append('path')
        .attr('id', 'arc-' + id)
        .attr('class', 'arc')
        .style('fill', 'orange')
        .style('opacity', 0.5);

      path
        .attr('d', arc.arc)
        .attr('transform', 'translate(' + arc.p1[0] + ',' + arc.p1[1] + ')');

      line
        .enter()
        .append('line')
        .attr('id', 'arc-' + id)
        .attr('class', 'arc');

      line
        .attr('x1', function(d) {
          return d.p1[0];
        })
        .attr('y1', function(d) {
          return d.p1[1];
        })
        .attr('x2', function(d) {
          return d.p2[0];
        })
        .attr('y2', function(d) {
          return d.p2[1];
        })
        .attr('stroke', 'black')
        .attr('stroke-width', 2);

      drag
        .on('drag', function(d, i) {
          var ev = d3.event,
            angle = 0;

          i = i + 2;

          arc['p' + i][0] = Math.min(Math.max(arc.minX + 1, ev.x), arc.maxX);
          arc['p' + i][1] = Math.min(Math.max(arc.minY, ev.y), arc.maxY);

          angle = i === 3 ? arcs.startAngle(id) : arcs.endAngle(id);

          if (
            (arc.startAngle < Math.PI &&
              arc.endAngle < Math.PI &&
              angle < Math.PI) ||
            (arc.startAngle >= Math.PI &&
              arc.endAngle >= Math.PI &&
              angle >= Math.PI)
          ) {
            if (i === 2) {
              arc.endAngle = angle;
              arc.arc.endAngle(angle);
            } else if (i === 3) {
              arc.startAngle = angle;
              arc.arc.startAngle(angle);
            }
          }

          drawStrum(arc, i - 2);
        })
        .on('dragend', onDragEnd());

      circles
        .enter()
        .append('circle')
        .attr('id', 'arc-' + id)
        .attr('class', 'arc');

      circles
        .attr('cx', function(d) {
          return d[0];
        })
        .attr('cy', function(d) {
          return d[1];
        })
        .attr('r', 5)
        .style('opacity', function(d, i) {
          return activePoint !== undefined && i === activePoint ? 0.8 : 0;
        })
        .on('mouseover', function() {
          d3.select(this).style('opacity', 0.8);
        })
        .on('mouseout', function() {
          d3.select(this).style('opacity', 0);
        })
        .call(drag);
    }

    function dimensionsForPoint(p) {
      var dims = { i: -1, left: undefined, right: undefined };
      d3.keys(__.dimensions).some(function(dim, i) {
        if (xscale(dim) < p[0]) {
          var next = d3.keys(__.dimensions)[
            pc.getOrderedDimensionKeys().indexOf(dim) + 1
          ];
          dims.i = i;
          dims.left = dim;
          dims.right = next;
          return false;
        }
        return true;
      });

      if (dims.left === undefined) {
        // Event on the left side of the first axis.
        dims.i = 0;
        dims.left = pc.getOrderedDimensionKeys()[0];
        dims.right = pc.getOrderedDimensionKeys()[1];
      } else if (dims.right === undefined) {
        // Event on the right side of the last axis
        dims.i = d3.keys(__.dimensions).length - 1;
        dims.right = dims.left;
        dims.left = pc.getOrderedDimensionKeys()[
          d3.keys(__.dimensions).length - 2
        ];
      }

      return dims;
    }

    function onDragStart() {
      // First we need to determine between which two axes the arc was started.
      // This will determine the freedom of movement, because a arc can
      // logically only happen between two axes, so no movement outside these axes
      // should be allowed.
      return function() {
        var p = d3.mouse(strumRect[0][0]),
          dims,
          arc;

        p[0] = p[0] - __.margin.left;
        p[1] = p[1] - __.margin.top;

        (dims = dimensionsForPoint(p)),
          (arc = {
            p1: p,
            dims: dims,
            minX: xscale(dims.left),
            maxX: xscale(dims.right),
            minY: 0,
            maxY: h(),
            startAngle: undefined,
            endAngle: undefined,
            arc: d3.svg.arc().innerRadius(0),
          });

        arcs[dims.i] = arc;
        arcs.active = dims.i;

        // Make sure that the point is within the bounds
        arc.p1[0] = Math.min(Math.max(arc.minX, p[0]), arc.maxX);
        arc.p2 = arc.p1.slice();
        arc.p3 = arc.p1.slice();
      };
    }

    function onDrag() {
      return function() {
        var ev = d3.event,
          arc = arcs[arcs.active];

        // Make sure that the point is within the bounds
        arc.p2[0] = Math.min(
          Math.max(arc.minX + 1, ev.x - __.margin.left),
          arc.maxX,
        );
        arc.p2[1] = Math.min(
          Math.max(arc.minY, ev.y - __.margin.top),
          arc.maxY,
        );
        arc.p3 = arc.p2.slice();
        //      console.log(arcs.angle(arcs.active));
        //      console.log(signedAngle(arcs.unsignedAngle(arcs.active)));
        drawStrum(arc, 1);
      };
    }

    // some helper functions
    function hypothenuse(a, b) {
      return Math.sqrt(a * a + b * b);
    }

    var rad = (function() {
      var c = Math.PI / 180;
      return function(angle) {
        return angle * c;
      };
    })();

    var deg = (function() {
      var c = 180 / Math.PI;
      return function(angle) {
        return angle * c;
      };
    })();

    // [0, 2*PI] -> [-PI/2, PI/2]
    var signedAngle = function(angle) {
      var ret = angle;
      if (angle > Math.PI) {
        ret = angle - 1.5 * Math.PI;
        ret = angle - 1.5 * Math.PI;
      } else {
        ret = angle - 0.5 * Math.PI;
        ret = angle - 0.5 * Math.PI;
      }
      return -ret;
    };

    /**
     * angles are stored in radians from in [0, 2*PI], where 0 in 12 o'clock.
     * However, one can only select lines from 0 to PI, so we compute the
     * 'signed' angle, where 0 is the horizontal line (3 o'clock), and +/- PI/2
     * are 12 and 6 o'clock respectively.
     */
    function containmentTest(arc) {
      var startAngle = signedAngle(arc.startAngle);
      var endAngle = signedAngle(arc.endAngle);

      if (startAngle > endAngle) {
        var tmp = startAngle;
        startAngle = endAngle;
        endAngle = tmp;
      }

      // test if segment angle is contained in angle interval
      return function(a) {
        if (a >= startAngle && a <= endAngle) {
          return true;
        }

        return false;
      };
    }

    function selected() {
      var ids = Object.getOwnPropertyNames(arcs),
        brushed = __.data;

      // Get the ids of the currently active arcs.
      ids = ids.filter(function(d) {
        return !isNaN(d);
      });

      function crossesStrum(d, id) {
        var arc = arcs[id],
          test = containmentTest(arc),
          d1 = arc.dims.left,
          d2 = arc.dims.right,
          y1 = __.dimensions[d1].yscale,
          y2 = __.dimensions[d2].yscale,
          a = arcs.width(id),
          b = y1(d[d1]) - y2(d[d2]),
          c = hypothenuse(a, b),
          angle = Math.asin(b / c); // rad in [-PI/2, PI/2]
        return test(angle);
      }

      if (ids.length === 0) {
        return brushed;
      }

      return brushed.filter(function(d) {
        switch (brush.predicate) {
          case 'AND':
            return ids.every(function(id) {
              return crossesStrum(d, id);
            });
          case 'OR':
            return ids.some(function(id) {
              return crossesStrum(d, id);
            });
          default:
            throw 'Unknown brush predicate ' + __.brushPredicate;
        }
      });
    }

    function removeStrum() {
      var arc = arcs[arcs.active],
        svg = pc.selection.select('svg').select('g#arcs');

      delete arcs[arcs.active];
      arcs.active = undefined;
      svg.selectAll('line#arc-' + arc.dims.i).remove();
      svg.selectAll('circle#arc-' + arc.dims.i).remove();
      svg.selectAll('path#arc-' + arc.dims.i).remove();
    }

    function onDragEnd() {
      return function() {
        var brushed = __.data,
          arc = arcs[arcs.active];

        // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
        // considered a drag without move. So we have to deal with that case
        if (arc && arc.p1[0] === arc.p2[0] && arc.p1[1] === arc.p2[1]) {
          removeStrum(arcs);
        }

        if (arc) {
          var angle = arcs.startAngle(arcs.active);

          arc.startAngle = angle;
          arc.endAngle = angle;
          arc.arc
            .outerRadius(arcs.length(arcs.active))
            .startAngle(angle)
            .endAngle(angle);
        }

        brushed = selected(arcs);
        arcs.active = undefined;
        __.brushed = brushed;
        pc.renderBrushed();
        events.brushend.call(pc, __.brushed);
      };
    }

    function brushReset(arcs) {
      return function() {
        var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
          return !isNaN(d);
        });

        ids.forEach(function(d) {
          arcs.active = d;
          removeStrum(arcs);
        });
        onDragEnd(arcs)();
      };
    }

    function install() {
      var drag = d3.behavior.drag();

      // Map of current arcs. arcs are stored per segment of the PC. A segment,
      // being the area between two axes. The left most area is indexed at 0.
      arcs.active = undefined;
      // Returns the width of the PC segment where currently a arc is being
      // placed. NOTE: even though they are evenly spaced in our current
      // implementation, we keep for when non-even spaced segments are supported as
      // well.
      arcs.width = function(id) {
        var arc = arcs[id];

        if (arc === undefined) {
          return undefined;
        }

        return arc.maxX - arc.minX;
      };

      // returns angles in [-PI/2, PI/2]
      angle = function(p1, p2) {
        var a = p1[0] - p2[0],
          b = p1[1] - p2[1],
          c = hypothenuse(a, b);

        return Math.asin(b / c);
      };

      // returns angles in [0, 2 * PI]
      arcs.endAngle = function(id) {
        var arc = arcs[id];
        if (arc === undefined) {
          return undefined;
        }
        var sAngle = angle(arc.p1, arc.p2),
          uAngle = -sAngle + Math.PI / 2;

        if (arc.p1[0] > arc.p2[0]) {
          uAngle = 2 * Math.PI - uAngle;
        }

        return uAngle;
      };

      arcs.startAngle = function(id) {
        var arc = arcs[id];
        if (arc === undefined) {
          return undefined;
        }

        var sAngle = angle(arc.p1, arc.p3),
          uAngle = -sAngle + Math.PI / 2;

        if (arc.p1[0] > arc.p3[0]) {
          uAngle = 2 * Math.PI - uAngle;
        }

        return uAngle;
      };

      arcs.length = function(id) {
        var arc = arcs[id];

        if (arc === undefined) {
          return undefined;
        }

        var a = arc.p1[0] - arc.p2[0],
          b = arc.p1[1] - arc.p2[1],
          c = hypothenuse(a, b);

        return c;
      };

      pc.on('axesreorder.arcs', function() {
        var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
          return !isNaN(d);
        });

        // Checks if the first dimension is directly left of the second dimension.
        function consecutive(first, second) {
          var length = d3.keys(__.dimensions).length;
          return d3.keys(__.dimensions).some(function(d, i) {
            return d === first
              ? i + i < length && __.dimensions[i + 1] === second
              : false;
          });
        }

        if (ids.length > 0) {
          // We have some arcs, which might need to be removed.
          ids.forEach(function(d) {
            var dims = arcs[d].dims;
            arcs.active = d;
            // If the two dimensions of the current arc are not next to each other
            // any more, than we'll need to remove the arc. Otherwise we keep it.
            if (!consecutive(dims.left, dims.right)) {
              removeStrum(arcs);
            }
          });
          onDragEnd(arcs)();
        }
      });

      // Add a new svg group in which we draw the arcs.
      pc.selection
        .select('svg')
        .append('g')
        .attr('id', 'arcs')
        .attr(
          'transform',
          'translate(' + __.margin.left + ',' + __.margin.top + ')',
        );

      // Install the required brushReset function
      pc.brushReset = brushReset(arcs);

      drag
        .on('dragstart', onDragStart(arcs))
        .on('drag', onDrag(arcs))
        .on('dragend', onDragEnd(arcs));

      // NOTE: The styling needs to be done here and not in the css. This is because
      //       for 1D brushing, the canvas layers should not listen to
      //       pointer-events.
      strumRect = pc.selection
        .select('svg')
        .insert('rect', 'g#arcs')
        .attr('id', 'arc-events')
        .attr('x', __.margin.left)
        .attr('y', __.margin.top)
        .attr('width', w())
        .attr('height', h() + 2)
        .style('opacity', 0)
        .call(drag);
    }

    brush.modes['angular'] = {
      install: install,
      uninstall: function() {
        pc.selection
          .select('svg')
          .select('g#arcs')
          .remove();
        pc.selection
          .select('svg')
          .select('rect#arc-events')
          .remove();
        pc.on('axesreorder.arcs', undefined);
        delete pc.brushReset;

        strumRect = undefined;
      },
      selected: selected,
      brushState: function() {
        return arcs;
      },
    };
  })();
  pc.interactive = function() {
    flags.interactive = true;
    return this;
  };

  // expose a few objects
  pc.xscale = xscale;
  pc.ctx = ctx;
  pc.canvas = canvas;
  pc.g = function() {
    return g;
  };

  // rescale for height, width and margins
  // TODO currently assumes chart is brushable, and destroys old brushes
  pc.resize = function() {
    // selection size
    pc.selection
      .select('svg')
      .attr('width', __.width)
      .attr('height', __.height);
    pc.svg.attr(
      'transform',
      'translate(' + __.margin.left + ',' + __.margin.top + ')',
    );

    // FIXME: the current brush state should pass through
    if (flags.brushable) pc.brushReset();

    // scales
    pc.autoscale();

    // axes, destroys old brushes.
    if (g) pc.createAxes();
    if (flags.brushable) pc.brushable();
    if (flags.reorderable) pc.reorderable();

    events.resize.call(this, {
      width: __.width,
      height: __.height,
      margin: __.margin,
    });
    return this;
  };

  // highlight an array of data
  pc.highlight = function(data) {
    if (arguments.length === 0) {
      return __.highlighted;
    }

    __.highlighted = data;
    pc.clear('highlight');
    d3.selectAll([canvas.foreground, canvas.brushed]).classed('faded', true);
    data.forEach(path_highlight);
    events.highlight.call(this, data);
    return this;
  };

  // clear highlighting
  pc.unhighlight = function() {
    __.highlighted = [];
    pc.clear('highlight');
    d3.selectAll([canvas.foreground, canvas.brushed]).classed('faded', false);
    return this;
  };

  // calculate 2d intersection of line a->b with line c->d
  // points are objects with x and y properties
  pc.intersection = function(a, b, c, d) {
    return {
      x:
        ((a.x * b.y - a.y * b.x) * (c.x - d.x) -
          (a.x - b.x) * (c.x * d.y - c.y * d.x)) /
        ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
      y:
        ((a.x * b.y - a.y * b.x) * (c.y - d.y) -
          (a.y - b.y) * (c.x * d.y - c.y * d.x)) /
        ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
    };
  };

  function position(d) {
    if (xscale.range().length === 0) {
      xscale.rangePoints([0, w()], 1);
    }
    var v = dragging[d];
    return v == null ? xscale(d) : v;
  }

  // Merges the canvases and SVG elements into one canvas element which is then passed into the callback
  // (so you can choose to save it to disk, etc.)
  pc.mergeParcoords = function(callback) {
    // Retina display, etc.
    var devicePixelRatio = window.devicePixelRatio || 1;

    // Create a canvas element to store the merged canvases
    var mergedCanvas = document.createElement('canvas');
    mergedCanvas.width = pc.canvas.foreground.clientWidth * devicePixelRatio;
    mergedCanvas.height =
      (pc.canvas.foreground.clientHeight + 30) * devicePixelRatio;
    mergedCanvas.style.width = mergedCanvas.width / devicePixelRatio + 'px';
    mergedCanvas.style.height = mergedCanvas.height / devicePixelRatio + 'px';

    // Give the canvas a white background
    var context = mergedCanvas.getContext('2d');
    context.fillStyle = '#ffffff';
    context.fillRect(0, 0, mergedCanvas.width, mergedCanvas.height);

    // Merge all the canvases
    for (var key in pc.canvas) {
      context.drawImage(
        pc.canvas[key],
        0,
        24 * devicePixelRatio,
        mergedCanvas.width,
        mergedCanvas.height - 30 * devicePixelRatio,
      );
    }

    // Add SVG elements to canvas
    var DOMURL = window.URL || window.webkitURL || window;
    var serializer = new XMLSerializer();
    var svgStr = serializer.serializeToString(pc.selection.select('svg')[0][0]);

    // Create a Data URI.
    var src = 'data:image/svg+xml;base64,' + window.btoa(svgStr);
    var img = new Image();
    img.onload = function() {
      context.drawImage(
        img,
        0,
        0,
        img.width * devicePixelRatio,
        img.height * devicePixelRatio,
      );
      if (typeof callback === 'function') {
        callback(mergedCanvas);
      }
    };
    img.src = src;
  };
  pc.version = '0.7.0';
  // this descriptive text should live with other introspective methods
  pc.toString = function() {
    return (
      'Parallel Coordinates: ' +
      d3.keys(__.dimensions).length +
      ' dimensions (' +
      d3.keys(__.data[0]).length +
      ' total) , ' +
      __.data.length +
      ' rows'
    );
  };

  return pc;
}

d3.renderQueue = function(func) {
  var _queue = [], // data to be rendered
    _rate = 10, // number of calls per frame
    _clear = function() {}, // clearing function
    _i = 0; // current iteration

  var rq = function(data) {
    if (data) rq.data(data);
    rq.invalidate();
    _clear();
    rq.render();
  };

  rq.render = function() {
    _i = 0;
    var valid = true;
    rq.invalidate = function() {
      valid = false;
    };

    function doFrame() {
      if (!valid) return true;
      if (_i > _queue.length) return true;

      // Typical d3 behavior is to pass a data item *and* its index. As the
      // render queue splits the original data set, we'll have to be slightly
      // more carefull about passing the correct index with the data item.
      var end = Math.min(_i + _rate, _queue.length);
      for (var i = _i; i < end; i++) {
        func(_queue[i], i);
      }
      _i += _rate;
    }

    d3.timer(doFrame);
  };

  rq.data = function(data) {
    rq.invalidate();
    _queue = data.slice(0);
    return rq;
  };

  rq.rate = function(value) {
    if (!arguments.length) return _rate;
    _rate = value;
    return rq;
  };

  rq.remaining = function() {
    return _queue.length - _i;
  };

  // clear the canvas
  rq.clear = function(func) {
    if (!arguments.length) {
      _clear();
      return rq;
    }
    _clear = func;
    return rq;
  };

  rq.invalidate = function() {};

  return rq;
};
